﻿package br.gov.bcb.app.stuff.util.props;

import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.List;

import net.sf.cglib.proxy.Enhancer;
import net.sf.cglib.proxy.MethodInterceptor;
import net.sf.cglib.proxy.MethodProxy;

import org.apache.commons.lang.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * Controlador e interceptor para {@link PropertyUtils}.
 */
public class PropertyUtilsControl implements MethodInterceptor {

    private static final Logger LOGGER = LoggerFactory.getLogger(PropertyUtilsControl.class);

    private static PropertyUtilsControl instancia = new PropertyUtilsControl();
    private static final int LENGTH_IS = 2;
    private static final int LENGTH_GET = 3;

    private ThreadLocal<List<String>> lastPropertyCalled = new ThreadLocal<List<String>>();

    /**
     * Construtor padrão. É protegido pois esta classe implementa um Singleton.
     */
    protected PropertyUtilsControl() {
        //singleton - contrutor protegido
    }

    /**
     * Retorna a instância desta classe.
     */
    public static PropertyUtilsControl get() {

        return instancia;
    }

    /**
     * Determina instância a ser retornada por {@link PropertyUtilsControl#get()}.
     * <p>
     * <strong>Este método deve ser chamado apenas por classes de teste.</strong>
     */
    protected static void set(PropertyUtilsControl control) {

        instancia = control;
    }

    /**
     * Retorna o objeto a ser utilizado para obter os nomes das propriedades.
     * 
     * @param <T> - tipo do objeto
     * @param clazz - classe do objeto
     */
    public <T> T createPropertyObject(Class<T> clazz) {

        Enhancer enhancer = new Enhancer();
        enhancer.setSuperclass(clazz);
        enhancer.setCallback(this);
        if (clazz.getName().equals(BigDecimal.class.getName())) {
            return clazz.cast(enhancer.create(new Class<?>[] {String.class}, new Object[] {"0"}));
        }
        return clazz.cast(enhancer.create());
    }

    /**
     * {@inheritDoc}.
     */
    public Object intercept(Object obj, Method method, Object[] args, MethodProxy proxy)
    //CHECKSTYLE:OFF - lançamento de Throwable definido na interface MethodInterceptor
            throws Throwable {
        //CHECKSTYLE:ON

        String name = method.getName();
        String property = getPropertyName(name);

        if (property != null) {
            getPropertyList().add(StringUtils.uncapitalize(property));
            return getReturn(method);
        }
        LOGGER.debug("O método chamado não é um getter: [" + name + "]");
        return method.invoke(obj, args);
    }

    /**
     * Obtém o nome da última propriedade chamada neste controlador.
     */
    protected String getLastPropertyCalled() {

        StringBuilder builder = new StringBuilder();
        List<String> propertyList = getPropertyList();
        for (String prop : propertyList) {
            builder.append(prop);
            builder.append(".");
        }
        return builder.substring(0, builder.length() - 1);
    }

    /**
     * Reinicia a pilha interna que acumula diferentes propriedades em chamadas encadeadas.
     */
    protected void clear() {
        lastPropertyCalled.set(new ArrayList<String>());
    }

    private Object getReturn(Method method) {

        Class<?> returnType = method.getReturnType();
        if (Modifier.isFinal(returnType.getModifiers())) {
            return null;
        }
        return createPropertyObject(returnType);
    }

    private String getPropertyName(String name) {

        if (name.startsWith("get")) {
            return name.substring(LENGTH_GET);
        } else if (name.startsWith("is")) {
            return name.substring(LENGTH_IS);
        }
        return null;
    }

    private List<String> getPropertyList() {

        List<String> propertyList = lastPropertyCalled.get();
        if (propertyList == null) {
            propertyList = new ArrayList<String>();
            lastPropertyCalled.set(propertyList);
        }
        return propertyList;
    }
}
